﻿namespace Rialternative.Repositories
{
    using System;
    using System.Data;
    using System.Data.Metadata.Edm;
    using System.Data.Objects;
    using System.Data.Objects.DataClasses;
    using System.Linq;
    using System.ServiceModel.DomainServices.EntityFramework;

    /// <summary>
    /// Provides generic data access logic.
    /// </summary>
    /// <typeparam name="TContext">The type of the data context.</typeparam>
    public class EntityFrameworkRepository<TContext> : IRepository where TContext : ObjectContext, new()
    {
        #region Fields
        /// <summary>
        /// The data context.
        /// </summary>
        private readonly TContext context;
        #endregion

        #region Constructor
        /// <summary>
        /// Initializes a new instance of the <see cref="EntityFrameworkRepository&lt;TContext&gt;"/> class.
        /// </summary>
        public EntityFrameworkRepository()
        {
            this.context = new TContext();
        }
        #endregion Constructor

        #region Finalizer
        /// <summary>
        /// Finalizes an instance of the <see cref="EntityFrameworkRepository&lt;TContext&gt;"/> class.
        /// Releases unmanaged resources and performs other cleanup operations before the
        /// <see cref="EntityFrameworkRepository&lt;TContext&gt;"/> is reclaimed by garbage collection.
        /// </summary>
        ~EntityFrameworkRepository()
        {
            this.Dispose(false);
        }
        #endregion Finalizer

        #region IRepository Members
        /// <summary>
        /// Gets the query.
        /// </summary>
        /// <typeparam name="TEntity">The type of the entity.</typeparam>
        /// <returns>A queryable object relative to the entity.</returns>
        public IQueryable<TEntity> GetQuery<TEntity>() where TEntity : class
        {
            return this.context.CreateObjectSet<TEntity>().AsQueryable();
        }

        /// <summary>
        /// Inserts the specified entity.
        /// </summary>
        /// <typeparam name="TEntity">The type of the entity.</typeparam>
        /// <param name="entity">The entity.</param>
        public void Insert<TEntity>(TEntity entity) where TEntity : class
        {
            EntityObject entityObject = entity as EntityObject;
            if (entityObject == null)
            {
                throw new ArgumentException("Not an EntityObject", "entity");
            }

            if (entityObject.EntityState != EntityState.Detached)
            {
                this.context.ObjectStateManager.ChangeObjectState(entity, EntityState.Added);
            }
            else
            {
                this.context.AddObject(this.GetEntitySetName(typeof(TEntity)), entity);
            }
        }

        /// <summary>
        /// Updates the specified entity.
        /// </summary>
        /// <typeparam name="TEntity">The type of the entity.</typeparam>
        /// <param name="entity">The entity.</param>
        /// <param name="original">The original version of the entity.</param>
        public void Update<TEntity>(TEntity entity, TEntity original) where TEntity : class
        {
            EntityObject entityObject = entity as EntityObject;
            if (entityObject == null)
            {
                throw new ArgumentException("Not an EntityObject", "entity");
            }

            this.context.CreateObjectSet<TEntity>().AttachAsModified(entity, original);
        }

        /// <summary>
        /// Deletes the specified entity.
        /// </summary>
        /// <typeparam name="TEntity">The type of the entity.</typeparam>
        /// <param name="entity">The entity.</param>
        public void Delete<TEntity>(TEntity entity) where TEntity : class
        {
            object originalItem;
            EntityKey key = this.context.CreateEntityKey(this.GetEntitySetName(typeof(TEntity)), entity);
            if (this.context.TryGetObjectByKey(key, out originalItem))
            {
                this.context.DeleteObject(originalItem);
            }
        }

        /// <summary>
        /// Persists changes.
        /// </summary>
        public void Persist()
        {
            this.context.SaveChanges();
        }
        #endregion IRepository Members

        #region IDisposable Members
        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Releases unmanaged and - optionally - managed resources.
        /// </summary>
        /// <param name="disposing">Release both managed and unmanaged resources if <c>true</c>; <c>false</c> to release only unmanaged resources.</param>
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                this.context.Dispose();
            }
        }
        #endregion IDisposable Members

        #region Private methods
        /// <summary>
        /// Gets the name of the entity set.
        /// </summary>
        /// <param name="entityType">Type of the entity.</param>
        /// <returns>The name of the entity set.</returns>
        private string GetEntitySetName(Type entityType)
        {
            while (entityType.BaseType != typeof(EntityObject))
            {
                entityType = entityType.BaseType;
            }

            EntityContainer container = this.context.MetadataWorkspace.GetEntityContainer(this.context.DefaultContainerName, DataSpace.CSpace);
            string entitySetName = (from meta in container.BaseEntitySets
                                    where meta.ElementType.Name == entityType.Name
                                    select meta.Name).FirstOrDefault();
            if (entitySetName == null)
            {
                throw new ArgumentException("No corresponding entityset name was found", "entityType");
            }

            return entitySetName;
        }
        #endregion Private methods
    }
}
